#!/usr/bin/python
# -*- encoding: utf-8 -*-
#
# Copyright (C) 2008 Håvard Gulldahl.
#
# Licensed under the GNU Public License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.gnu.org/licenses/gpl-2.0.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__author__ = u'<havard@gulldahl.no> Håvard Gulldahl'
__license__ = 'GPL v2'
DEBUG = False

#import pdb
import sys, time, os.path, logging
from StringIO import StringIO
from email.utils import parseaddr
from PyQt4 import QtCore, QtGui, uic
import atom

import gdata.contacts.service as service
import gdata.contacts as contacts
import gcontacts.gcontactsbase as gcontactsbase

import bitpimlite.vcard as bitpim
from gcontacts import vcard as liteVCard,  mscontact,  dialog as Dialog, gcontacts_rc
#import gcontacts
from gcontacts.contactsmodel import ContactsListModel
from gcontacts.contactsagent import ContactsAgent


if DEBUG:
  import httplib
  httplib.HTTPConnection.debuglevel=1  ## verbosely output http traffic

UI_DIR = ''
if True:
  UI_DIR = os.path.join('gcontacts',  'data')
  sys.path.insert(0,  'gcontacts')

PASS_FILE = 'gcontacts_pass.txt'

if sys.platform == 'win32':
  PASS_FILE = os.path.join(os.getenv('HOMEDRIVE'), os.getenv('HOMEPATH'),  '_gcontacts' )
else:
  PASS_FILE = os.path.join(os.path.expanduser('~'),  '.gcontactspass')

class GContacts(QtGui.QDialog):
  loggedIn = False
  stdout = None
  vcard_mimetypes = ['text/directory; charset="utf-8"; profile="vCard"', 'text/x-vcard']
  messageBox = None
  groups = None

  def __init__(self, model, agent):
    QtGui.QDialog.__init__(self)
    self.gui = uic.loadUi(os.path.join(UI_DIR, 'gcontacts.ui'), self)
    self.contactModel = model
    self.proxyModel = QtGui.QSortFilterProxyModel(self)
    self.proxyModel.setSourceModel(self.contactModel)
    self.proxyModel.setFilterKeyColumn(-1) #filter (search) on all columns
    self.proxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
    self.gui.contactList.setModel(self.proxyModel)
    #self.gui.contactList.setModel(model)
    self.contactAgent = agent
    self.connect(self.gui.pushButton, QtCore.SIGNAL('clicked()'), self.loginOrClear)
    self.connect(self.gui.filter, QtCore.SIGNAL('textChanged(QString)'), self.proxyModel.setFilterFixedString)
    self.connect(self.gui.contactGroup, QtCore.SIGNAL('currentIndexChanged(int)'), self.gui.filterGroup)
    #self.connect(self.gui.contactList, QtCore.SIGNAL('pressed(QModelIndex)'), self.contactsMouseEvent)
    self.connect(self.proxyModel, QtCore.SIGNAL('rowsRemoved(QModelIndex, int, int)'), self.updateFilter)
    self.connect(self.proxyModel, QtCore.SIGNAL('rowsInserted(QModelIndex, int, int)'), self.updateFilter)
    self.connect(model, QtCore.SIGNAL('layoutChanged()'), self.updateProgress)
    self.connect(model, QtCore.SIGNAL('droppedVCard(PyQt_PyObject)'), self.newVCard)
    self.connect(model, QtCore.SIGNAL('droppedUrl(PyQt_PyObject)'), self.newUrl)
    self.connect(model, QtCore.SIGNAL('droppedAddress(PyQt_PyObject)'), self.newAddress)
    self.connect(model, QtCore.SIGNAL('rowsAdded(PyQt_PyObject)'), self.selectRows)

  def enableControls(self,  toggle=True):
    self.gui.pushButton.setEnabled(toggle)
    self.gui.contactList.setEnabled(toggle)
    self.gui.filter.setEnabled(toggle)
    self.gui.contactGroup.setEnabled(toggle)
    
  def loginOrClear(self):
    if self.loggedIn:
      return self.clearInputBox()
    else:
      self.gui.pushButton.setText('&Clear')
      return self.login()

  def login(self):
    try:
      user, pword = open(PASS_FILE).readline().split(',')
    except IOError:
      user, pword = self.loginDialog()
    if not user:
      self.alert('Username not supplied')
      return False
    if not pword:
      self.alert('Password not supplied')
      return False

    self.emit(QtCore.SIGNAL('login(PyQt_PyObject,PyQt_PyObject)'), user, pword)
    #disable controls
    self.enableControls(False)

  def didLogin(self, success):
    # success will be False if login failed
    # enable widgets
    self.enableControls(True)
    if success:
      self.loggedIn = True
      self.emit(QtCore.SIGNAL('getContacts(PyQt_PyObject,PyQt_PyObject)'), 1, 25)
      self.emit(QtCore.SIGNAL('getContactGroups()'))

  def updateProgress(self):
    loaded = len(self.contactModel.entries)
    total = self.contactModel.total_results
    self.gui.progressBar.setMaximum(total)
    self.gui.progressBar.setValue(loaded)
    self.gui.progressBar.setToolTip("%s of %s contacts loaded" % (loaded, total))
    self.contactListMessage("%s of %s contacts loaded" % (loaded, total))

  def updateFilter(self, idx, start, end):
    #logging.debug(self.proxyModel.rowCount())
    self.contactListMessage('Showing %s matches' % self.proxyModel.rowCount())
    
  def clearInputBox(self):
    return self.gui.filter.clear()

  def contactsMouseEvent(self, index):
    buttons = QtGui.QApplication.mouseButtons()
    if buttons == QtCore.Qt.NoButton:
      return # mouse move event
    indexes = [self.proxyModel.mapToSource(idx) for idx in self.gui.contactList.selectedIndexes()]
    rows = set([i.row() for i in indexes])
    if not rows:
      return # no rows selected
    if buttons == QtCore.Qt.LeftButton:
      logging.debug("left %s", rows)
    elif buttons == QtCore.Qt.RightButton:
      logging.debug("right %s", rows)
      menu = QtGui.QMenu(self)
      menu.setTitle(u"Edit items")
      menu.addAction("Delete %s items" % len(rows), lambda: self.deleteIndexes(indexes))
      menu.exec_(self.gui.contactList.mapToGlobal(QtCore.QPoint(0,0)))

  def selectRows(self, indexes):
    selection = self.gui.contactList.selectionModel()
    selection.clear()
    for i in indexes:
      selection.select(i, QtGui.QItemSelectionModel.SelectCurrent | QtGui.QItemSelectionModel.Columns)

  def loginDialog(self):
    dialog = Dialog.PasswordDialog()

    res = dialog.exec_()
    if res == dialog.Accepted:
      return (unicode(dialog.user.text()),
        unicode(dialog.password.text()))
    else:
      raise Exception("Password dialog was cancelled")

  def conflictDialog(self, contact1, contact2):
    dialog = Dialog.ConflictDialog()

    dialog.contact1Label.setText(contact1.ToVCard().decode('utf8'))
    dialog.contact2Label.setText(contact2.ToVCard())
    res = dialog.exec_()
    return res == dialog.Accepted

  def deleteIndexes(self, indexes):
    rows = set([i.row() for i in indexes])
    logging.debug('Deleting indexes %s?', indexes)
    if self.yesno('Do you really want to delete %s items?' % len(rows)):
      self.contactModel.doDeleteIndexes(indexes)

  def alert(self, msg):
    QtGui.QMessageBox.critical(self, 'Failure', unicode(msg))

  def yesno(self, msg):
    question = QtGui.QMessageBox.question(self, 'Question', msg, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
    return question == QtGui.QMessageBox.Yes

  def contactListMessage(self, s, source='auto'):
    logging.debug('setting message (%s): %s', source, s)
    if not self.messageBox:
      self.messageBox = QtGui.QLabel(self.gui.contactList)
      self.messageBox.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignRight)
      #self.messageBox.move(0,0)
      f = self.messageBox.font()
      f.setPointSize(8)
      self.messageBox.setFont(f)
    self.messageBox.setText(s)
      
  def contactsRemoved(self, contactslist):
    return self.contactListMessage('%s contacts deleted' % len(contactslist), 'user')

  def loadGroups(self, groupfeed):
    #logging.debug(groupfeed)
    self.groups = groupfeed
    groupicon = QtGui.QIcon()
    groupicon.addPixmap(QtGui.QPixmap(":/graphics/config-users.svg"), QtGui.QIcon.Normal, QtGui.QIcon.On)
    for group in groupfeed.entry:
      self.contactGroup.addItem(groupicon, group.title.text, QtCore.QVariant(group.ToString()))

  def filterGroup(self, idx):
    logging.debug('filgergroup %s', idx)
    if idx == 0:
      self.contactModel.doLoadGroup(None) # show all contacts
      #self.proxyModel.setFilterFixedString('')
      return
    try:
      groupxml = unicode(self.contactGroup.itemData(idx).toString())
    except AttributeError, e:
      logging.exception(e)
      return False
    #print groupxml
    group = gcontactsbase.ContactEntryFromString(groupxml.encode('utf8'))
    groupuri = group.id.text
    
    logging.debug('filtergroup:%s-> %s', idx, groupuri)
    self.contactModel.doLoadGroup(groupuri)
    #self.proxyModel.setFilterFixedString('')

  def newVCard(self, data):
    logging.debug("newVCard: %s", type(data))
    if not self.loggedIn: #not logged in
      self.alert("You need to log in first, you know")
      return False
    if not hasattr(data,  'read'):
      data = StringIO(data)
    vcards = list(liteVCard.VCards(bitpim.VFile(data)))
    print vcards
    
    for vcard in vcards:
      vdata = vcard.getdata()
      logging.debug("==================bitpim.VCARD====================\n%s\n============================", vdata)
      rel = {'business': contacts.REL_WORK,
             'home': contacts.REL_HOME,
             'other': contacts.REL_OTHER}
      name = atom.Title(text=vcard.getfield('name').pop())
      emails = []
      first = True
      for email in vcard.getfield('email'):
        emails.append(gcontactsbase.Email(address=email,
          primary=(first and 'true') or None))
        first = False
      phones = []
      # XXX: These consts really should be in gdata.contacts!
      phone_rel = { 'business': gcontactsbase.PHONE_WORK,
                    'business fax': gcontactsbase.PHONE_WORK_FAX,
                    'cell': gcontactsbase.PHONE_MOBILE }
      phone_rel.update(rel)
      first = True
      for tel in vcard.getfield('phone'):
        phones.append(gcontactsbase.PhoneNumber(text=tel['number'],
          rel=phone_rel.get(tel['type'], gcontactsbase.PHONE_GENERAL),
          primary=(first and 'true') or None))
        first = False
      addresses = []
      first = True
      for address in vcard.getfield('address'):
        addresses.append(gcontactsbase.PostalAddress(text=address['street'],
          rel=rel.get(address['type'], contacts.REL_OTHER),
          primary=(first and 'true') or None))
        first = False
      im_rel = {  'AIM': 'http://schemas.google.com/g/2005#AIM', # AOL Instant Messenger
                  'MSN': 'http://schemas.google.com/g/2005#MSN', #    MSN Messenger
                  'YAHOO': 'http://schemas.google.com/g/2005#YAHOO', #  Yahoo Messenger
                  'SKYPE': 'http://schemas.google.com/g/2005#SKYPE', #  Skype
                  'QQ': 'http://schemas.google.com/g/2005#QQ', #     QQ
                  'GOOGLE_TALK': 'http://schemas.google.com/g/2005#GOOGLE_TALK', # Google Talk
                  'ICQ': 'http://schemas.google.com/g/2005#ICQ', #    ICQ
                  'JABBER': 'http://schemas.google.com/g/2005#JABBER', #     Jabber
                }
      ims = []
      first = True
      for im in vcard.getfield('im'):
        _im = gcontactsbase.IM(address=im['address'],
          rel=rel.get(im['type'], contacts.REL_OTHER),
          primary=(first and 'true') or None)
        try:
          _im.protocol = im_rel[im['protocol']]
        except KeyError:
          _im.label = u'Protocol: %s' % im['protocol']
          _im.rel = None ## Cannot have both label and rel
        ims.append(_im)
        first = False
      comment = None
      #if vcard.getfield('uid'):
        #comment = atom.Content(text='UID=%s\n' % vcard.getfield('uid').pop())
      if vcard.getfield('notes'):
        comment = atom.Content(text=vcard.getfield('notes').pop())
      org = title = organiz = None
      if vcard.getfield('title'):
        title = contacts.OrgTitle(text=vcard.getfield('title').pop())
      if vcard.getfield('organisation'):
        org = contacts.OrgName(text=vcard.getfield('organisation').pop())
      if org or title:
        organiz = gcontactsbase.Organization(org_name = org, org_title=title)
      contact = gcontactsbase.ContactEntry(title=name, email=emails,
        phone_number=phones, postal_address=addresses, im=ims, content=comment,
        organization=organiz,
        )
      return self.new(contact)

  def newUrl(self,  url):  # a QUrl object
    logging.debug("newUrl: %s", url)
    contactfile = unicode(url.toLocalFile()) # TODO: support for non-file:// urls
    if contactfile.endswith('.contact'):
      logging.debug("new from vista contact")
      contact = mscontact.MSContactEntry()
      try:
        contact.loadMSContact(contactfile)
      except Exception, e:
        logging.exception(e)
        return False
      return self.new(contact)
    elif contactfile.endswith('.vcf') or contactfile.endswith('.vcard'):
      logging.debug("new from VCard file")
      return self.newVCard(file(contactfile).read())
    else:
      logging.error("unknown url: %s",  contactfile)

  def newAddress(self,  address):
    "add new contact from 'name <address@example.com>' or 'address@example.com (name)'"
    for addr in address.split(','):
      try:
        name,  email = parseaddr(addr) # from email.utils
      except Exception,  e:
        raise e
      assert '@' in email,  "Invalid email address (no @ sign)"
      nom = atom.Title(text=name)
      mail = gcontactsbase.Email(address=email, primary='true')
      return self.new(gcontactsbase.ContactEntry(title=nom, email=[mail]))

  def new(self,  contact):
    logging.debug("==================GContacts.Entry====================\n%s\n============================", contact)
    try:
      newEntry = self.contactAgent.contactsclient.Post(contact,
                    '/m8/feeds/contacts/default/full',
                     converter=gcontactsbase.ContactEntryFromString)
    except Exception, e:
      logging.debug('Error: %s', e)
      try:
        ex = e.message
        if ex.has_key('status') and ex['status'] == 409:
          # Conflict : A contact with this email already exists
          currentEntry = gcontactsbase.ContactEntryFromString(ex['body'].decode('utf8'))
          if self.conflictDialog(currentEntry, contact):
            updatedEntry = self.contactAgent.contactsclient.Put(contact,
                                currentEntry.GetEditLink().href,
                                converter=gcontactsbase.ContactEntryFromString)
            logging.info('updated entry: %s', updatedEntry)
        else:
          self.alert(e.message['reason'])
      except AttributeError, e:
        logging.exception(e)
        self.alert(str(e))
      #self.alert('%s: %s\n%s' % (e.status, e.reason, e.body))
    else:
      logging.info('inserting new entry: %s', newEntry)
      self.contactModel.doLoadContactEntries([newEntry, ])

def txt(obj):
  'Extracts text from elementtree construct. Always returns utf8'
  if hasattr(obj, 'text'):
    obj = obj.text
  if obj is None: return u''
  return obj.decode('utf8')

def version():
  import gcontacts
  return gcontacts.__version__

if __name__ == "__main__":
  DEBUG = '-d' in sys.argv
  if DEBUG:
    logging.basicConfig(level=logging.DEBUG)
  app = QtGui.QApplication(sys.argv)
  app.setStartDragDistance(10)
  model = ContactsListModel()
  agent = ContactsAgent()
  gcontacts = GContacts(model, agent)
  gcontacts.gui.show()
  model.proxy = agent

  app.connect(agent, QtCore.SIGNAL('error(PyQt_PyObject)'), gcontacts.gui.alert)
  app.connect(gcontacts.gui, QtCore.SIGNAL('login(PyQt_PyObject,PyQt_PyObject)'), agent.doLogin)
  app.connect(agent, QtCore.SIGNAL('loggedIn(PyQt_PyObject)'), gcontacts.didLogin)
  app.connect(gcontacts.gui, QtCore.SIGNAL('getContacts(PyQt_PyObject,PyQt_PyObject)'), agent.doGetContacts)
  app.connect(gcontacts.gui, QtCore.SIGNAL('getContactGroups()'), agent.doGetGroups)
  app.connect(model, QtCore.SIGNAL('getContacts(PyQt_PyObject,PyQt_PyObject)'), agent.doGetContacts)
  app.connect(agent, QtCore.SIGNAL('gotContactsFeed(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), model.doLoadContactsFeed)
  app.connect(agent, QtCore.SIGNAL('gotGroupsFeed(PyQt_PyObject)'), gcontacts.loadGroups)
  app.connect(agent, QtCore.SIGNAL('gotGroupsFeed(PyQt_PyObject)'), model.doParseGroupFeed)
  app.connect(agent, QtCore.SIGNAL("deleted(PyQt_PyObject)"), model.doRemoveContact)
  app.connect(agent, QtCore.SIGNAL("deletedContacts(PyQt_PyObject)"), model.doRemoveContacts)
  app.connect(agent, QtCore.SIGNAL("deletedContacts(PyQt_PyObject)"), gcontacts.contactsRemoved)
  app.connect(gcontacts.gui.trash, QtCore.SIGNAL('deleteContacts(PyQt_PyObject)'), agent.doDeleteContacts)

  sys.exit(app.exec_())
